﻿/*
 * GX - Full-Featured Javascript Animations Framework v1.2 - by Riccardo Degni (RD)
 *
 * Copyright (c) 2009 Riccardo Degni (http://www.riccardodegni.net/)
 * MIT-Style License
 */

// Global Methods	
var Fns = {
    Create: function (props) {
        var props = props || {};
        var fn = function () {
            return (this.init) ? this.init.apply(this, arguments) : this;
        };

        for (var prop in props) fn.prototype[prop] = props[prop];
        return fn;
    },

    Bind: function (fn, bind, args) {
        return function () {
            fn.apply(bind, args || []);
        };
    },

    Contains: function (obj, el) {
        for (var i = 0; i < obj.length; i++)
            if (obj[i] == el) return true;
        return false;
    },

    Camelize: function (str) {
        str = str.replace(/-\D/g, function (match) {
            return match.charAt(1).toUpperCase();
        });
        return str;
    },

    Now: function () {
        return new Date().getTime();
    },

    Extend: function (base, more, merge) {
        if (typeof base != 'object' && typeof base != 'function') base = {};
        for (var p in more)
            if (!base[p] || merge) base[p] = more[p];
        return base;
    },

    Each: function (obj, fn) {
        for (var i = 0; i < obj.length; i++) fn.apply(obj, [obj[i], i, obj]);
    }
};

// The GX Wrapper
var GX = Fns.Create({
    options: {
        duration: 1000,
        fps: 50,
        defaultUnit: 'px',
        queue: 'queue',
        easing: 'Linear',
        delay: false
    },

    init: function (element, opts) {
        this.element = element;
        this.options = Fns.Extend(opts, GX.prototype.options);
        return this;
    },

    anime: function (styles, duration, easing, callback) {
        if (!this.isRunning) {
            this.isRunning = true;
            this.styles = styles;
            this.duration = (duration) ? ((typeof duration == 'string') ? GX.durations[Fns.Camelize(duration)] : duration) : this.options.duration;
            this.easing = easing || this.options.easing;
            this.callback = ((typeof callback == 'object') ? callback.complete : ((typeof callback == 'function') ? callback : false));
            this.startFn = (typeof callback == 'object') ? callback.start : false;
            if (!this.chain) this.chain = [];
            this.interval = Math.round(1000 / this.options.fps);

            this.starts = {};
            this.ends = {};
            this.changes = {};
            this.sizes = {};
            this.units = {};

            // compute values
            for (var style in this.styles) {
                var camelStyle = Fns.Camelize(style), camelStyle = (GX.complex.hasOwnProperty(camelStyle)) ? GX.complex[camelStyle] : camelStyle, jStyle = this.styles[style], jUnit = this.options.defaultUnit || 'px', jChanges = false;
                var cssStyle = this.element.css(camelStyle), startStyle = parseFloat((cssStyle == 'auto') ? 0 : cssStyle);
                if ((Fns.Contains(GX.axis, style)) && this.element.css('position') == 'static') this.element.css('position', 'relative');

                if (GX.Color.isColor(style)) { // '#ff00ff' | '#f0f' | [255, 0, 255]
                    startStyle = GX.Color.cssToRgb(this.element.css(camelStyle));
                    jStyle = GX.Color.cssToRgb(this.styles[style]);
                    jChanges = [jStyle[0] - startStyle[0], jStyle[1] - startStyle[1], jStyle[2] - startStyle[2]];
                }
                else if (typeof jStyle == 'string') {
                    if (Fns.Contains(GX.specialValues, jStyle)) { // 'show' | 'hide | 'toggle'
                        if (!this.element.data('gxSave_' + camelStyle)) this.element.data('gxSave_' + camelStyle, cssStyle);
                        var to = parseFloat(this.element.data('gxSave_' + camelStyle)) || 1;
                        switch (jStyle) {
                            case 'show': this.styles[style] = jStyle = to; break;
                            case 'hide': this.styles[style] = jStyle = 0; break;
                            case 'toggle': this.styles[style] = jStyle = (parseFloat(Math.round(startStyle)) != 0) ? 0 : to; break;
                        }
                    }
                    else { // '200px' | '200' | '+=200px' | '+=200'
                        var fullStyle = GX.Parse.style(jStyle, jUnit);
                        if (typeof fullStyle == 'object') {	// '200px' | '400em' | ...
                            this.styles[style] = jStyle = parseFloat(fullStyle[0]);
                            jUnit = fullStyle[1] || 'px';
                            // relative animations
                            if (fullStyle[2]) // '+=' or '-='
                                this.styles[style] = jStyle = (fullStyle[2] == '+=') ? (jStyle + startStyle) : (startStyle - jStyle);
                        }
                        else if (typeof fullStyle == 'string') { // '400' | '20' | ...
                            this.styles[style] = jStyle = parseFloat(fullStyle);
                        }
                    }
                }

                this.starts[style] = startStyle;
                this.ends[style] = jStyle;
                this.changes[style] = jChanges || this.ends[style] - this.starts[style];
                this.units[style] = jUnit;
            }

            if (this.startFn) this.startFn.apply(this, [this.element, this]);
            this.time = Fns.Now();
            this.timer = setInterval(Fns.Bind(this.increase, this), this.interval);
        }
        else {
            if (this.options.queue == 'queue') {
                var boundAnime = Fns.Bind(this.anime, this, arguments);
                this.chain.push(boundAnime);
            }
            else if (this.options.queue == 'cancel') {
                this.clearTimer();
                this.anime.apply(this, arguments);
            }
        }
        return this;
    },

    increase: function () {
        var elapsedTime = this.elapsedTime = Fns.Now() - this.time;
        if (elapsedTime < this.duration) {
            for (var style in this.styles) {
                var easing = this.easing.split(':'), easingType = (easing[1]) ? easing[1] : 'InOut', ease = GX.Transitions[easing[0]][easingType];
                var starts = this.starts[style], changes = this.changes[style];
                if (typeof starts != 'object') {
                    this.sizes[style] = ease(elapsedTime, starts, changes, this.duration);
                    if (this.sizes[style] < 0 && !Fns.Contains(GX.axis, style)) this.sizes[style] = 0; // prevent css warnings
                }
                else { // colors
                    this.sizes[style] = [ease(elapsedTime, starts[0], changes[0], this.duration),
										 ease(elapsedTime, starts[1], changes[1], this.duration),
										 ease(elapsedTime, starts[2], changes[2], this.duration)];
                }
            }
        }
        else {
            this.clearTimer();
            for (var style in this.styles) {
                this.sizes[style] = (GX.Color.isColor(style)) ? this.ends[style] : this.styles[style];
            }
        }
        this.setStyles();
    },

    parseStyle: function (style, sz) {
        var camelStyle = Fns.Camelize(style);
        // ? color value : opacity or integers
        (GX.Color.isColor(camelStyle)) ? this.element.css(camelStyle, 'rgb(' + parseInt(sz[0]) + ',' + parseInt(sz[1]) + ',' + parseInt(sz[2]) + ')') : (this.element.css(camelStyle, (camelStyle == 'opacity') ? sz : sz + this.units[style]));
    },

    clearTimer: function () {
        this.isRunning = false;
        this.timer = clearInterval(this.timer);
    },

    pause: function () {
        this.clearTimer();
    },

    resume: function () {
        this.isRunning = true;
        this.time = Fns.Now() - this.elapsedTime;
        this.timer = setInterval(Fns.Bind(this.increase, this), this.interval);
    },

    setStyles: function () {
        for (var style in this.styles) {
            this.parseStyle(style, this.sizes[style]);
        }
        if (!this.isRunning) {
            if (this.callback && typeof this.callback == 'function') this.callback.apply(this, [this.element, this]);
            var delay = this.options.delay, chain = this.chain, ring = function () { chain.shift()(); };
            if (chain.length != 0) (!delay) ? ring() : setTimeout(ring, delay);
        }
    }
});

GX.Parse = {
    style: function (s, un) {
        var fullStyle = [], value, unit, relative, relatives = ['+=', '-='];

        Fns.Each(relatives, function (rel, i) {
            if (s.indexOf(rel) != -1) {
                relative = rel;
                s = s.replace(rel, '');
            }
        });

        Fns.Each(GX.units, function (u, i) {
            if (s.indexOf(u) != -1) {
                value = parseFloat(s);
                unit = u;
                fullStyle.push(value, unit);
            }
        });

        if (!unit) {
            value = parseFloat(s);
            unit = un;
            fullStyle.push(value, unit);
        }

        if (relative) fullStyle.push(relative);
        return (fullStyle.length > 0) ? fullStyle : s;
    }
};

// Global Methods for dealing with colors
GX.Color = {
    decToHex: function (dec) {
        return dec.toString(16);
    },

    hexToDec: function (hex) {
        return parseInt(hex, 16);
    },

    rgbToHex: function (r, g, b) {
        var dth = GX.Color.decToHex;
        return [dth(r), dth(g), dth(b)];
    },

    hexToRgb: function (h, e, x) {
        var htd = GX.Color.hexToDec;
        return [htd(h), htd(e), htd(x)];
    },

    cssToRgb: function (color) {
        if (GX.Color.customColors[color]) return GX.Color.customColors[color]; // 'red' | 'blue' | ...

        if (typeof color == 'object' && color.length == 3) return color; // [255, 0, 255]

        if (color.indexOf('rgb') <= -1) { // #ff00ff | #f0f
            var color = (color.length > 4) ? color : GX.Color.shortToFull(color);
            return GX.Color.hexToRgb(color.substring(1, 3), color.substring(3, 5), color.substring(5, 7));
        }

        var col = color.substring(4, color.length - 1).split(','), nCol = [];
        Fns.Each(col, function (c) {
            nCol.push(parseInt(c));
        });
        return nCol;
    },

    shortToFull: function (color) {
        var r = color.charAt(1), g = color.charAt(2), b = color.charAt(3);
        return '#' + r + r + g + g + b + b;
    },

    isColor: function (style) {
        return (style.toLowerCase().indexOf('color') != -1);
    },

    customColors: {
        red: [255, 0, 0],
        green: [0, 255, 0],
        blue: [0, 0, 255],
        white: [255, 255, 255],
        black: [0, 0, 0]
    }
};

// Internal GXs
GX.linear = function (t, b, c, d) { return c * t / d + b; };
Fns.Extend(GX, {
    Transitions: { Linear: { 'In': GX.linear, 'Out': GX.linear, 'InOut': GX.linear } }, // Base Transition
    units: ['px', 'em', '%', 'in', 'pt', 'ex'],
    durations: { 'verySlow': 4000, 'slow': 2000, 'normal': 1000, 'fast': 500, 'veryFast': 250 },
    specialValues: ['show', 'hide', 'toggle'],
    complex: { 'borderWidth': 'borderTopWidth', 'borderColor': 'borderTopColor', 'margin': 'marginTop', 'padding': 'paddingTop' },
    axis: ['top', 'left'],
    unlink: function (obj) {
        var end = {};
        switch (typeof obj) {
            case 'object': for (var p in obj) end[p] = GX.unlink(obj[p]);
                break;
            default: return obj;
        }
        return end;
    }
});

(function (Jx) {
    Jx.fn.extend({
        setGX: function (el) {
            if (!el.data('gx')) el.data('gx', new GX().init(el, {}));
            return el;
        },

        gxInit: function (opts) {
            var set = Jx(this), jq = this;
            Fns.Each(set, function (el) {
                var el = jq.setGX(Jx(el));
                Fns.Extend(el.data('gx').options, opts, true);
            });
            return this;
        },

        an: function (styles, duration, easing, callback) {
            var set = Jx(this), jq = this;
            Fns.Each(set, function (el) {
                var el = jq.setGX(Jx(el)), gx = el.data('gx');
                (typeof styles == 'string') ? gx[styles]() : gx.anime(GX.unlink(styles), duration, easing, callback);
            });
            return this;
        }
    });
})(Jx);